#@+leo-ver=5-thin
#@+node:slzatz.20111124212740.1516: * @file toodledo.py
#@@nowrap
#@@language python
#@@tabwidth -4
#@+others
#@+node:slzatz.20100314151332.3212: ** Module Imports
#shamelessly stolen with a few mods from felix riedel's poodledo
import os
import urllib2
import urllib
import StringIO
import shutil

from xml.etree import ElementTree as ET
import time
import datetime
import sys

if sys.version < 2.5:
     from md5 import md5
else:
    from hashlib import md5

import lmglobals as g

print_ = g.logger.write

print_("Hello from the toodledo module")


__all__ = ['ApiClient']

#@+node:slzatz.20100314151332.3213: ** converters
#@+node:slzatz.20100314151332.3214: *3* _local_date
def _local_date(string):
    # note format is: Sun, 11 Jan 2009 21:26:55 -0600  [appears to be in midwest?]
    dt = datetime.datetime.strptime(string[0:25], '%a, %d %b %Y %H:%M:%S')
    return dt #+ datetime.timedelta(hours=6) + datetime.timedelta(seconds=_local_time_offset())

#@+node:slzatz.20100314151332.3215: *3* _local_time_offset
def _local_time_offset():
    """Return offset of local zone from GMT"""
    if time.localtime().tm_isdst and time.daylight:
        return -time.altzone
    else:
        return -time.timezone

#@+node:slzatz.20100314151332.3216: *3* _date
def _date(string):
    return datetime.date(*time.strptime(string,"%Y-%m-%d")[0:3])

#@+node:slzatz.20100314151332.3217: *3* _datetime
def _datetime(string):

    try:
        d = datetime.datetime.strptime(string,"%Y-%m-%d %H:%M:%S")
    except ValueError:
        d = datetime.datetime.fromordinal(1)

    return d

#@+node:slzatz.20100314151332.3218: *3* _unix_datetime
def _unix_datetime(string):
    return datetime.datetime.fromtimestamp(float(string))

#@+node:slzatz.20100314151332.3219: *3* _boolstr
def _boolstr(string):
    return bool(int(string))

#@+node:slzatz.20100314151332.3220: *3* _attrib
def _attrib(node, atb):
    return node.attrib[atb]

#@+node:slzatz.20100314151332.3221: *3* _time (not in use)
#def _time(string):
#    return datetime.time(hour[, minute[, second[, microsecond[, tzinfo]]]])
#@+node:slzatz.20100314151332.3222: ** class ToodledoError
class ToodledoError(Exception):
    ''' Error return from Toodledo API server'''
    #@+others
    #@+node:slzatz.20100314151332.3223: *3* __init__
    def __init__(self, error_msg):
        self.msg = error_msg

    #@+node:slzatz.20100314151332.3224: *3* __str__
    def __str__(self):
        return "Toodledo server returned error: %s" % self.msg

    #@-others
#@+node:slzatz.20100314151332.3225: ** class ToodledoData
class ToodledoData(object):
    #@+others
    #@+node:slzatz.20100314151332.3226: *3* _typemap
    _typemap = {
            'server': {
                'unixtime': _unix_datetime,
                'date': _local_date,
                'serveroffset': int,
                'tokenexpires': float
                },
            'folder': {
                'id': int,
                'archived': _boolstr,
                'private': _boolstr,
                'order': int
                },
            'context': {
                'id': int,
                'def': _boolstr,
                },
            'goal': {
                'id': int,
                'level': int,
                'contributes': int,
                'archived': _boolstr
                },
            'account': {
                'userid': str,
                'alias': str,
                'pro': _boolstr,
                'dateformat': int,
                'timezone': int,
                'hidemonths': int,
                'hotlistpriority': int,
                'hotlistduedate': int,
                'lastaddedit': _datetime,
                'lastdelete': _datetime,
                'lastfolderedit': _datetime,
                'lastcontextedit': _datetime,
                'lastgoaledit': str,
                'lastnotebookedit': str,
                },
            'task': {
                'id': int,
                'parent': int,
                'children': int,
                'title': unicode,
                'tag': str,
                'folder': int,
                'context':  str,
                'context_id': int, # not in use yet
                'reminder': int, ### new
                'goal': str,
                'goal_id': int,
                'added': _date,
                'modified': _datetime,
                'startdate': str,
                'duedate': _date,
                'duedate_modifier': str,
                'starttime': str,
                'duetime': str,
                'completed': _date,
                'repeat': int,
                'rep_advanced': str,
                'status': int,
                'star': _boolstr,
                'priority': int,
                'length': int,
                'timer': int,
                'timer_onfor': str,
                'note': unicode, 
                'stamp': _datetime #for deleted doesn't really below here but should work
                },              
            'deleted': {  # this doesn't work because of how list works
                'id': int,
                'stamp': _datetime, 
                }
              }


    #@+node:slzatz.20100314151332.3227: *3* __init__
    def __init__(self,node=None):
        typemap = ToodledoData._typemap[node.tag]
        for elem in node:  #node.getchildren():
        # I think you can do for elem in node:
            if elem.text:
                self.__dict__[elem.tag] = typemap[elem.tag](elem.text)
            else:
                self.__dict__[elem.tag] = None

            # this turns the context id attrib any other node attribs of tasks into context_id 
            for a in elem.attrib:
                self.__dict__[elem.tag+'_'+a] = typemap[elem.tag+'_'+a](elem.attrib[a])

        for a in node.attrib: # this is how folders get their dict set since their data is in attrib
            self.__dict__[a] = typemap[a](node.attrib[a])

        if not 'title' in self.__dict__: # title is in task but need this for folders, contexts
            if node.text:
                self.title = node.text

    #@+node:slzatz.20100314151332.3228: *3* __repr__
    def __repr__(self):
        return str(self.__dict__)


    #@-others
#@+node:slzatz.20100314151332.3229: ** class PoodledoError
class PoodledoError(Exception):
    #@+others
    #@+node:slzatz.20100314151332.3230: *3* __init__
    def __init__(self, msg):
        self.msg = msg

    #@+node:slzatz.20100314151332.3231: *3* __repr__
    def __repr__(self):
        return 'PoodledoError("%s")' % self.msg

    #@+node:slzatz.20100314151332.3232: *3* __str__
    def __str__(self):
        return self.msg

    #@-others
#@+node:slzatz.20100314151332.3233: ** check_api_key
def check_api_key(f):
    ''' A decorator that makes the decorated function check for a API key'''
    def fn(*args, **kwargs): #sz change added args was fn(self,**kwargs)
        # check if key is set to a value
        if 'key' in kwargs and kwargs['key'] is not None:
            return f(*args, **kwargs)
        else:
            # try to get the key from the ApiClient
            if args[0].key is not None:
                kwargs['key'] = args[0].key
                return f(*args, **kwargs)
            else:
                raise PoodledoError('need API key to call function %s' % f.__name__)
    return fn

#@+node:slzatz.20100314151332.3234: ** returns_list
def returns_list(f):
    def fn(self, **kwargs):
        return [ ToodledoData(elem) for elem in f(self, **kwargs) ]
    return fn

#@+node:slzatz.20100314151332.3235: ** returns_item
def returns_item(f):
    def fn(self, **kwargs):
        return ToodledoData(f(self, **kwargs))
    return fn

#@+node:slzatz.20100314151332.3236: ** class ApiClient
class ApiClient(object):
    ''' Toodledo API client'''
    _SERVICE_URL = 'http://www.toodledo.com/api.php?'

    #@+others
    #@+node:slzatz.20100314151332.3237: *3* __init__
    def __init__(self, key=None):
        ''' Initializes a new ApiClient w/o auth credentials'''
            
        self._urlopener = urllib2.build_opener()

        self.key = key
        #self.token = None
        self.userid = None
        self.appid = None

    #@+node:slzatz.20100314151332.3238: *3* set_urlopener
    def set_urlopener(self, opener):
        self._urlopener = opener


    #@+node:slzatz.20100314151332.3239: *3* _create_url
    def _create_url(self,**kwargs):
        ''' Creates a GET url by appending key-value pairs to the SERVICE_URL'''

        url = ApiClient._SERVICE_URL

        # add args to url (key1=value1;key2=value2)
        # trailing underscores are stripped from keys to allow keys like pass_

        url += ';'.join(key.rstrip('_') + '=' + str(kwargs[key]) for key in sorted(kwargs))

        return url

    #@+node:slzatz.20101224095938.2743: *3* _create_url2
    def _create_url2(self,**kwargs):
        ''' Creates a POST url by using urllib2.Request(url, data)'''

        # note that all the urls sent could be posts

        url = ApiClient._SERVICE_URL

        #request =  urllib2.Request(url[, data][, headers][, origin_req_host][, unverifiable])
        #the HTTP request will be a POST instead of a GET when the data parameter is provided. 
        #data should be a buffer in the standard application/x-www-form-urlencoded format. 
        #The urllib.urlencode() function takes a mapping or sequence of 2-tuples and returns a string in this format.

        # urllib.encode is not necessary and actually screws things up

        # creates data of form -- key1=value1&key2=value2

        data= '&'.join(key.rstrip('_') + '=' + str(kwargs[key]) for key in sorted(kwargs))

        url = urllib2.Request(url, data)

        return url

    #@+node:slzatz.20100314151332.3240: *3* _check_for_error
    def _check_for_error(self, node):
        if node.tag == 'error':
            raise ToodledoError(node.text)


    #@+node:slzatz.20100314151332.3241: *3* _call
    def _call(self, **kwargs):
        '''calls the GET method _create_url #1'''

        url = self._create_url(**kwargs)
        print_("(1) url={0}".format(url))

        stream = self._urlopener.open(url)
        s = StringIO.StringIO() #
        shutil.copyfileobj(stream, s) #
        
        s.seek(0) # not sure this one is necessary
        print_("(1) stream={0}".format(s.read())) ######## March 18, 2012
        
        #root_node = ET.parse(stream).getroot()
        s.seek(0) # note you have to do this even if you don't read the file
        tree = ET.parse(s) #
        root_node = tree.getroot() #
        self._check_for_error(root_node)
        return root_node


    #@+node:slzatz.20101224095938.2745: *3* _call2
    def _call2(self, **kwargs):
        '''calls the POST method _create_url2 #2'''

        # note that all the URLs could be posts and _call2 and _createurl2 is all that's needed

        url = self._create_url2(**kwargs)
        print_("(2) url={0}".format(url))

        stream = self._urlopener.open(url)
        s = StringIO.StringIO()
        shutil.copyfileobj(stream, s)
        
        s.seek(0) # not sure this one is necessary
        print_("(2) stream={0}".format(s.read())) ######## March 18, 2012
        
        s.seek(0) # note you have to do this even if you don't read the file
        tree = ET.parse(s) #
        root_node = tree.getroot() #
        self._check_for_error(root_node)
        return root_node


    #@+node:slzatz.20100314151332.3242: *3* authenticate
    def authenticate(self, email, passwd, userid=None, appid=None):
        ''' Uses credentials to get userid, token and auth key'''
        if userid is None:
            userid = self.getUserid(email,passwd)

        token = self.getToken(userid, appid)
        self.key = self._generateKey(userid, token, passwd)
        return self.key

    #@+node:slzatz.20100314151332.3243: *3* isAuthenticated (property)
    @property
    def isAuthenticated(self):
        if self.key is None:
            return False

        try:
            result = self.getServerInfo()
        except ToodledoError, value:
            print value
            return False
        else:
            print "tokenexpires in ",result.tokenexpires, "minutes"
            return bool(float(result.tokenexpires) > 3.0)

        #return bool(self.key is not None)

    #@+node:slzatz.20100314151332.3244: *3* _generateKey
    def _generateKey(self, userid, token, passwd):
        ''' Generates a key as specified in the API docs'''
        return md5(md5(passwd).hexdigest() + token + userid).hexdigest()

    #@+node:slzatz.20100314151332.3245: *3* getUserid
    def getUserid(self, email, passwd):
        userid = self._call(method='getUserid', email=email, pass_=passwd).text
        if userid == '1':
            raise ToodledoError('invalid username/password')
        return userid 

    #@+node:slzatz.20100314151332.3246: *3* getToken
    def getToken(self, userid, appid=None):
        #if userid is None:
        #    if self.userid is not None:
        #        userid = self.userid
        #    else:
        #        raise Exception() # TODO: 

        if appid==None:
            return self._call(method='getToken', userid=userid).text
        else:
            return self._call(method='getToken', userid=userid, appid=appid).text

    #@+node:slzatz.20100314151332.3247: *3* getKey
    def getKey(self, userid=None, token=None, passwd=None):
        if userid is None:
            userid = self.userid
        if token is None:
            token = self.token
        if passwd is None:
            passwd = self.passwd

        self.key = self._generateKey(userid, token, passwd)

    #@+node:slzatz.20100314151332.3248: *3* getServerInfo
    @check_api_key
    @returns_item
    def getServerInfo(self, key=None):
        return self._call(method='getServerInfo', key=key)

    #@+node:slzatz.20100314151332.3249: *3* getAccountInfo
    @check_api_key
    @returns_item
    def getAccountInfo(self, key=None):
        return self._call(method='getAccountInfo', key=key)

    #@+node:slzatz.20100314151332.3250: *3* getFolders
    @check_api_key
    @returns_list
    def getFolders(self, key=None):
        return self._call(method='getFolders', key=key)

    #@+node:slzatz.20100314151332.3251: *3* getContexts
    @check_api_key
    @returns_list
    def getContexts(self, key=None):
        return self._call(method='getContexts', key=key)

    #@+node:slzatz.20100314151332.3252: *3* getGoals
    @check_api_key
    @returns_list
    def getGoals(self, key=None):
        return self._call(method='getGoals', key=key)

    #@+node:slzatz.20100314151332.3253: *3* getTasks
    @check_api_key
    @returns_list
    def getTasks(self, key=None, **kwargs):        
        return self._call(method='getTasks', key=key, **kwargs)

    #@+node:slzatz.20100314151332.3254: *3* getDeleted
    @check_api_key
    @returns_list
    def getDeleted(self, after, key=None):
        return self._call(method='getDeleted', key=key, after=after)

    #@+node:slzatz.20100314151332.3255: *3* addTask
    @check_api_key
    def addTask(self,key=None,**kwargs):
        return self._call2(method='addTask', key=key, **kwargs).text

    #@+node:slzatz.20100314151332.3256: *3* addContext
    @check_api_key
    def addContext(self,key=None,**kwargs):
        return self._call(method='addContext', key=key, **kwargs).text

    #@+node:slzatz.20100314151332.3257: *3* addGoal
    @check_api_key
    def addGoal(self,key=None,**kwargs):
        return self._call(method='addGoal', key=key, **kwargs).text

    #@+node:slzatz.20100314151332.3258: *3* addFolder
    @check_api_key
    def addFolder(self,key=None,**kwargs):
        return self._call(method='addFolder', key=key, **kwargs).text

    #@+node:slzatz.20100314151332.3259: *3* deleteFolder
    @check_api_key
    def deleteFolder(self, id_, key=None):
        return self._call(method='deleteFolder', id_=id_, key=key).text

    #@+node:slzatz.20100314151332.3260: *3* deleteContext
    @check_api_key
    def deleteContext(self, id_, key=None):
        return self._call(method='deleteContext', id_=id_, key=key).text

    #@+node:slzatz.20100314151332.3261: *3* deleteGoal
    @check_api_key
    def deleteGoal(self, id_, key=None):
        return self._call(method='deleteGoal', id_=id_, key=key).text

    #@+node:slzatz.20100314151332.3262: *3* deleteTask
    @check_api_key
    def deleteTask(self, id_, key=None):
        return self._call(method='deleteTask', id_=id_, key=key).text

    #@+node:slzatz.20100314151332.3263: *3* editFolder
    @check_api_key
    def editFolder(self, id_, key=None, **kwargs):
        return self._call(method='editFolder', id_=id_, key=key).text

    #@+node:slzatz.20100314151332.3264: *3* editTask
    @check_api_key
    def editTask(self, id_, key=None, **kwargs):
        return self._call2(method='editTask', id_=id_, key=key, **kwargs).text

    #@+node:slzatz.20100314151332.3265: *3* createAccount
    def createAccount(self, email, pass_):
        '''Create a new account

        Returns:
            userid - 15 or 16 character hexidecimal string
        '''
        return self._call(method='createAccount', email=_email, pass_=pass_).text
    #@-others
#@-others
#@-leo
